package com.xiaobai.service.addition;

import com.alibaba.fastjson.JSON;
import com.xiaobai.common.AddressEnum;
import com.xiaobai.common.DelayIntTask;
import com.xiaobai.dao.HealthInfoDao;
import com.xiaobai.dao.HealthLogDao;
import com.xiaobai.exception.ProxyException;
import com.xiaobai.pojo.HealthInfo;
import com.xiaobai.pojo.HealthLog;
import com.xiaobai.service.imageRecognition.IValidCode;
import com.xiaobai.units.HttpUnits;
import com.xiaobai.units.IpProxyUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @description: 健康上报,上学前假期上报
 * @author: 小白白
 * @create: 2022-01-13
 **/

//@Service
@Slf4j
public class HealthReportService_v3 implements IHealthReportService, ApplicationRunner, DisposableBean {

    /**
     * 本质都是一样的, 需要登录, 然后再提交数据即可
     * 1. 登录 借鉴v2版本
     * 2. 提交 借鉴v2版本, 改一下数据即可
     */

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private HealthInfoDao healthInfoDao;

    @Autowired
    @Qualifier("validCodeService") // pyValidCode  validCodeService
    private IValidCode validCode;

    @Autowired
    private HealthLogDao healthLogDao;

    // 图片存储地址
    public static String IMAGES_PATH;
    // 代理错误次数达到阈值,就取消代理
    private static AtomicInteger proxyErrorCount = new AtomicInteger(0);
    // 阈值为8
    private static int proxyThreshold = 8;
    private static Date errorProxyDate = new Date();

    private static ThreadLocal<RequestConfig> requestConfigThreadLocal = new ThreadLocal<>();
    private static RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(6000).build();
    private Map<Integer,Boolean> taskHaveMap = new HashMap<>();
    private DelayQueue<DelayIntTask> delayQueue = new DelayQueue<>();

    //    private static CloseableHttpClient httpclient = HttpClients.custom().setMaxConnTotal(10).setMaxConnPerRoute(10).build();
    public static CloseableHttpClient httpclient = HttpClients.createDefault();

    private static final String INDEX_URL = "http://210.38.250.128:1024/";
    private static final String LOGIN_URL = "http://210.38.250.128:1024//login/Login.htm";
    private static final String VALID_IMAGE_URL = "http://210.38.250.128:1024/nonlogin/login/captcha/isvalid.htm";
    private static final String IMAGE_CODE_PRE = "http://210.38.250.128:1024/nonlogin/login/captcha.htm?code=";
    private static final String UPLOAD_INFO = "http://210.38.250.128:1024/syt/zzapply/operation.htm";

    public String login(String username, String password) {
        // BASE64信息
        String base64username = Base64.getEncoder().encodeToString(username.getBytes());
        String base64password = Base64.getEncoder().encodeToString(password.getBytes());

        HttpGet httpGet = new HttpGet(INDEX_URL);
        RequestConfig requestConfig = requestConfigThreadLocal.get();
        httpGet.setConfig(requestConfig == null ? defaultRequestConfig : requestConfig);
        try(CloseableHttpResponse rsp = httpclient.execute(httpGet)) {
            validStatusCode(rsp);
            String jsessionid = rsp.getFirstHeader("Set-Cookie").getElements()[0].getValue();
            jsessionidThreadLocal.set(jsessionid);
            String token = rsp.getLastHeader("Set-Cookie").getElements()[0].getValue();
            tokenThreadLocal.set(token);
            // 存入username
            usernameThreadLocal.set(username);
        } catch (ProxyException e) {
            return "代理IP连接超时(系统错误)";
        } catch (IOException e) {
            // Connect to 210.38.250.128:1024 [/210.38.250.128] failed: connect timed out
            // Connect to 1.1.1.214:64257 [/1.1.1.214] failed: connect timed out
            log.error(e.getMessage());
            return validTimeout(e.getMessage(), "打卡官网登录超时");
        }

        String validCode = null;
        if (isValidCaptcha()){
            // 需要验证码
            validCode = getValidCode();
            if (validCode == null) {
                log.error("验证码返回为null");
                return "返回验证码为空(系统错误)";
            }
        }

        // 再去登录
        HttpPost httpPostLogin = new HttpPost(LOGIN_URL);
        httpPostLogin.setConfig(requestConfig == null ? defaultRequestConfig : requestConfig);
        fillPostHeader(httpPostLogin);
        Map<String,String> paramsMap = new HashMap<>();
        paramsMap.put("username",base64username);
        paramsMap.put("password",base64password);
        paramsMap.put("verification",validCode);
        paramsMap.put("token",tokenThreadLocal.get());
        httpPostLogin.setEntity(HttpUnits.generateEntity(paramsMap));
        try(CloseableHttpResponse rsp = httpclient.execute(httpPostLogin)) {
            validStatusCode(rsp);
            // 登录成功
            String content = EntityUtils.toString(rsp.getEntity());
            // 返回字符串如此
            if (content.equals("éª\u008Cè¯\u0081ç \u0081è¾\u0093å\u0085¥é\u0094\u0099è¯¯ï¼\u0081")){
                // 验证码错误
                log.error("验证码错误(系统错误)");
                this.validCode.cancelSave();
                return "验证码错误(系统错误)";
            }
            if (content.equals("ç\u0094¨æ\u0088·å\u0090\u008Dæ\u0088\u0096å¯\u0086ç \u0081é\u0094\u0099è¯¯!")){
                // 密码错误
                log.error("密码错误");
                return "密码错误";
            }
            // 有验证码就存储正确的验证码
            if (validCode != null) {
                this.validCode.saveImageFile();
            }
            return "密码正确";
        } catch (ProxyException e) {
            return "代理IP连接超时(系统错误)";
        } catch (IOException e) {
            e.printStackTrace();
            return validTimeout(e.getMessage(), "内部错误(系统错误)");
        }
    }

    public String mainExecuteV3(String username, String password, HealthInfoV3 healthInfoV3) {
        String loginResult = login(username, password);
        if (!StringUtils.equals("密码正确",loginResult)) {
            return loginResult;
        }
        // 去打卡
        HttpPost httpPostUpload = new HttpPost(UPLOAD_INFO);
        RequestConfig requestConfig = requestConfigThreadLocal.get();
        httpPostUpload.setConfig(requestConfig == null ? defaultRequestConfig : requestConfig);
        fillPostHeader(httpPostUpload);
        httpPostUpload.setHeader("Content-Type","application/x-www-form-urlencoded");
        httpPostUpload.setHeader("datatype","text");
        Map<String,String> loginParamsMap = new HashMap<>();
        loginParamsMap.put("data","{\"xmqkb\":{\"id\":\"52a67a007e304439017ee6442066494d\"},\"c1\":\"正常\",\"c2\":\"无上述症状\",\"c3\":\"否\",\"c5\":\"绿色\",\"c6\":\"否\",\"c7\":\"否\",\"c8\":\"广东省,东莞市,清溪镇\",\"type\":\"YQSJSB\",\"location_longitude\":114.163468,\"location_latitude\":22.821333,\"location_address\":\"广东省 东莞市 鹿城南路 6号 靠近清溪晨光学校 \"}");
        loginParamsMap.put("msgUrl", "syt/zzapply/list.htm?type=YQSJSB&xmid=52a67a007e304439017ee6442066494d");
        loginParamsMap.put("type", "YQSJSB");
        httpPostUpload.setEntity(HttpUnits.generateEntity(loginParamsMap));
        try(CloseableHttpResponse rsp = httpclient.execute(httpPostUpload)) {
            validStatusCode(rsp);
            String message = "打卡成功";
            String content = EntityUtils.toString(rsp.getEntity());
            switch (content) {
                case "ysp":
                    message = "已经审核不能再次修改！";
                    break;
                case "upperlimit":
                    message = "已达项目最大申请条数";
                    break;
                case "Applied today":
                    message = "今日已打过卡";
                    break;
                case "location_address is null":
                    message = "位置信息为空";
                    break;
            }
            return message;
        } catch (ProxyException e) {
            return "代理IP连接超时(系统错误)";
        }  catch (IOException e) {
            e.printStackTrace();
            return validTimeout(e.getMessage(), "内部错误(系统错误)");
        }
    }

    // post填充通用请求头
    private static void fillPostHeader(HttpPost httpPost) {
        httpPost.setHeader("Host","210.38.250.128:1024");
        httpPost.setHeader("Origin","http://210.38.250.128:1024");
        httpPost.setHeader("Referer","http://210.38.250.128:1024/");
        httpPost.setHeader("X-Requested-With","XMLHttpRequest");
        httpPost.setHeader("Cookie","username="+usernameThreadLocal.get()+"; menuVisible=0; JSESSIONID=" + jsessionidThreadLocal.get());
    }

    // get填充通用请求头
    private static void fillGetHeader(HttpGet httpGet) {
        httpGet.setHeader("Host","210.38.250.128:1024");
        httpGet.setHeader("Referer","http://210.38.250.128:1024/");
        httpGet.setHeader("Cookie","username="+usernameThreadLocal.get()+"; menuVisible=0; JSESSIONID=" + jsessionidThreadLocal.get());
    }

    // 是否需要验证码
    private static boolean isValidCaptcha() {
        HttpPost httpPost = new HttpPost(VALID_IMAGE_URL);
        RequestConfig requestConfig = requestConfigThreadLocal.get();
        httpPost.setConfig(requestConfig == null ? defaultRequestConfig : requestConfig);
        fillPostHeader(httpPost);
        try(CloseableHttpResponse response = httpclient.execute(httpPost)) {
            validStatusCode(response);
            String content = EntityUtils.toString(response.getEntity());
            return Boolean.valueOf(content);
        } catch (ProxyException e) {
            // 代理错误
        }  catch (IOException e) {
            String errorMsg = validTimeout(e.getMessage(), "自错误");
            if (!errorMsg.equals("自错误")) {
                log.error( "代理错误,判断需要验证码超时");
                // 默认不需要验证码
            } else {
                e.printStackTrace();
            }
        }
        return false;
    }

    // 请求验证码图片并返回验证码
    private String getValidCode() {
        String url = IMAGE_CODE_PRE + System.currentTimeMillis();
        HttpGet httpGet = new HttpGet(url);
        RequestConfig requestConfig = requestConfigThreadLocal.get();
        httpGet.setConfig(requestConfig == null ? defaultRequestConfig : requestConfig);
        fillGetHeader(httpGet);
        try (CloseableHttpResponse response = httpclient.execute(httpGet)){
            validStatusCode(response);
            byte[] bytes = EntityUtils.toByteArray(response.getEntity());
            // 暂时不使用python识别, 把付费的次数用完
            // return imageCodePython(bytes);
            return validCode.figureCaptcha(bytes);
        } catch (ProxyException e) {
            return "代理IP连接超时(系统错误)";
        }  catch (IOException e) {
            String errorMsg = validTimeout(e.getMessage(), "自错误");
            if (!errorMsg.equals("自错误")) {
                log.error( "代理错误,获取验证码超时");
            } else {
                e.printStackTrace();
            }
        }
        return null;
    }

    private static ThreadLocal<String> usernameThreadLocal = new ThreadLocal<>();
    private static ThreadLocal<String> tokenThreadLocal = new ThreadLocal<>();
    private static ThreadLocal<String> jsessionidThreadLocal = new ThreadLocal<>();

    public static void clearThreadLocal() {
        usernameThreadLocal.remove();
        tokenThreadLocal.remove();
        jsessionidThreadLocal.remove();
        requestConfigThreadLocal.remove();
    }

    @Override
    public String validLogin(String username, String password) {
        return login(username, password);
    }

    private boolean validDate() {
        Calendar calendar = Calendar.getInstance();
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        return hour < 20 && hour >= 8;
    }

    @Override
    public String doAction(String username, String password, String address, int count, boolean enableProxy) {
        throw new RuntimeException("不该走此");
    }

    public String doActionV3(String username, String password, HealthInfoV3 healthInfoV3, int count, boolean enableProxy) {
        String result;
        // 时间区间检测
        if (!validDate()) {
            result = "操作时间区间[08:00 ~ 20:00]";
        } else {
            // 启用代理 && 代理未降级 => 开启代理
            if (enableProxy) {
                if (proxyErrorCount.get() <= proxyThreshold) {
                    // 未达到阈值,才开启代理
                    fillProxyIp();
                } else {
                    log.info("代理降级,不开启代理");
                    // 检测距离上次超时是否有半小时了,有半小时,那么下次就重新开启代理
                    if (DateUtils.addMinutes(errorProxyDate, 30).getTime() < new Date().getTime()) {
                        proxyErrorCount.set(0);
                        log.info("代理降级恢复");
                    }
                }
            } else {
                // 不启动代理
            }
            result = mainExecuteV3(username, password, healthInfoV3);
        }
        // Q: 验证码错误(系统错误) 为什么插入不到数据库?
        // A: 因为是valid接口返回的数据
        HealthLog healthLog = new HealthLog();
        healthLog.setCreateTime(new Date());
        healthLog.setResult(result);
        healthLog.setStudentId(username);
        healthLogDao.insert(healthLog);
        // 错误信息包含系统错误就重来
        if (result.contains("系统错误") && count <= 5) {
            return this.doActionV3(username, password, healthInfoV3, count + 1, enableProxy);
        }

        // 等一会继续打
        if (count > 5 && result.contains("系统错误")) {
            this.addTaskByStudentId(username);
        }
        // 打完了主动清除
        clearThreadLocal();
        return result;
    }

    // 根据studentId加入打卡队列
    private void addTaskByStudentId(String studentId) {
        Integer id = healthInfoDao.selectIdByStudentId(studentId);
        // 15分钟后再打
        this.delayQueue.add(new DelayIntTask(id, DateUtils.addMinutes(new Date(), 15)));
    }

    private void fillProxyIp() {
        RequestConfig.Builder builder = RequestConfig.custom().setConnectTimeout(6000);
        IpProxyUtil.ProxyAddress proxyAddress = IpProxyUtil.getItem();
        builder.setProxy(new HttpHost(proxyAddress.getIp(), proxyAddress.getPort()));
        requestConfigThreadLocal.set(builder.build());
    }

    private static String validTimeout(String errorMsg, String myErrorMsg) {
        // Q: 代理访问一个的网址(超时了),返回的错误信息是超时的网址吗?
        // A: 是的
        if (errorMsg.contains("210.38.250.128")) {
            return myErrorMsg;
        } else {
            // 更新一次代理IP
            IpProxyUtil.updateItem();
            int count = proxyErrorCount.incrementAndGet();
            // 大于阈值了,记录当前的时间
            if (count > proxyThreshold) {
                errorProxyDate = new Date();
            }
            return "代理IP连接超时(系统错误)";
        }
    }

    private static void validStatusCode(CloseableHttpResponse response) throws ProxyException {
        int statusCode = response.getStatusLine().getStatusCode();
        // 代理错误
        if (statusCode == 401) {
            IpProxyUtil.updateItem();
            throw new ProxyException();
        }
    }

    @Override
    public void addDelayTask(DelayIntTask delayIntTask) {
        // 加入之前会自动remove
        this.removeDelayTask(delayIntTask.getId());
        this.delayQueue.add(delayIntTask);
        taskHaveMap.put(delayIntTask.getId(),true);
    }

    @Override
    public void removeDelayTask(int id) {
        if (taskHaveMap.get(id)!=null && taskHaveMap.get(id)) {
            this.delayQueue.remove(new DelayIntTask(id, new Date()));
        }
    }

    @Override
    public void doActionCover() {
        // 找出当天 未打卡成功 日志所拥有的 dis(student_id)
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.HOUR_OF_DAY,0);
        calendar.set(Calendar.MINUTE, 1);
        Date date = calendar.getTime();
        // 限定result:"验证码错误(系统错误)"
        List<HealthLog> list = healthLogDao.getFailListByToday(date);
        List<String> ids = list.stream().map(HealthLog::getStudentId).collect(Collectors.toList());
        List<HealthInfo> infoList = healthInfoDao.getListById(ids);
        for (HealthInfo info : infoList) {
            Integer addressId = info.getAddressId();
            String address = AddressEnum.getAddress(addressId, info.getAddress());
            this.doAction(info.getStudentId(), info.getPassword(), address, 0, true);
        }
    }

    private static final String TASK_KEY = "health_list";

    @Override
    public void run(ApplicationArguments args) {
        List<String> list = stringRedisTemplate.opsForList().range(TASK_KEY, 0, -1);
        stringRedisTemplate.delete(TASK_KEY);
        if (list != null && list.size() != 0) {
            for (String s : list) {
                DelayIntTask delayIntTask = JSON.parseObject(s, DelayIntTask.class);
                delayQueue.add(delayIntTask);
                taskHaveMap.put(delayIntTask.getId(),true);
            }
            log.info("预留任务个数:{}", list.size());
        }
        Thread thread = new Thread(()->{
            while (true) {
                try {
                    DelayIntTask intTask = delayQueue.take();
                    long startTime = System.currentTimeMillis();
                    HealthInfo healthInfo = healthInfoDao.selectByPrimaryKey(intTask.getId());
                    Integer addressId = healthInfo.getAddressId();
                    String address = AddressEnum.getAddress(addressId, healthInfo.getAddress());
                    this.doAction(healthInfo.getStudentId(), healthInfo.getPassword(), address, 0, true);
                    taskHaveMap.put(intTask.getId(),false);
                    log.info("学号:{},执行用时:{}s", healthInfo.getStudentId(), (System.currentTimeMillis() - startTime) / 1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }

    @Override
    public void destroy() {
        if (delayQueue.size() != 0) {
            Object[] objects = delayQueue.toArray();
            String[] strArr = new String[objects.length];
            for (int i = 0; i < strArr.length; i++) {
                strArr[i] = JSON.toJSONString(objects[i]);
            }
            stringRedisTemplate.opsForList().rightPushAll(TASK_KEY,strArr);
        }
    }

    @Override
    public int queueSize() {
        return this.delayQueue.size();
    }

    @Override
    public List<Integer> queueDataAndClear() {
        List<Integer> list = new ArrayList<>(delayQueue.size());
        for (DelayIntTask delayIntTask : delayQueue) {
            list.add(delayIntTask.getId());
        }
        delayQueue.clear();
        return list;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class HealthInfoV3 {

        public HealthInfoV3(String studentId, String c1, String c2, String c3, String c5, String c6, String c7, String c8, String type, String location_address) {
            this.studentId = studentId;
            this.c1 = c1;
            this.c2 = c2;
            this.c3 = c3;
            this.c5 = c5;
            this.c6 = c6;
            this.c7 = c7;
            this.c8 = c8;
            this.type = type;
            this.location_address = location_address;
        }

        private String studentId;
        private String c1;
        private String c2;
        private String c3;
        private String c5;
        private String c6;
        private String c7;
        // 省市区
        private String c8;
        private String type;
        // 经度
        private Double location_longitude;
        // 维度
        private Double location_latitude;
        // 定位地址 [广东省 东莞市 鹿城南路 6号 靠近清溪晨光学校 ]
        private String location_address;
    }

    /**
     * 1. 默认的keep-alive会再两次execute之间,复用TCP,但是如果对方的keep-alive太短,那么对方会主动断开,
     *    我这端会进入close-wait状态,进入close-wait状态后我方应用程序已经跑过了httpclient,
     *    在下一请求之前不会及时响应,会卡在close-wait状态很久。
     * 2. 可在请求头设置 "Connection" "close" 表明自己不支持长连接,对方会在响应后关闭TCP连接。
     * 3. close-wait后续会被pool自动清理吗?会自动清理程序主动关闭且正在close状态的TCP连接。
     * 4. 疑问: 不读取流就不会触发keepalive
     */
    public static void main(String[] args) throws IOException, InterruptedException {
//        HealthInfoV3 healthInfoV3 = new HealthInfoV3("正常", "无上述症状", "否", "绿色", "否", "否", "广东省,东莞市,清溪镇", "YQSJSB", 114.163468, 22.821333, "广东省 东莞市 鹿城南路 6号 靠近清溪晨光学校 ");
//        HealthReportService_v3 h = new HealthReportService_v3();
//        String s = h.doActionV3("18034520228", "Abcxyz123!", healthInfoV3, 1, false);
//        System.out.println(s);
    }


}
